#include "vanila/baseobject.h"
#include "vanila/dsobject.h"
#include "vanila/object.h"
#include "vanila/allocator.h"
#include "vanila/virtualmachine.h"
#include "vanila/garbagecollector.h"
#include "vanila/common.h"
#include "utils/hash.h"
#include "utils/format.h"
#include <cstdlib>
#include <string>
#include <iostream>
#include <map>
#include <set>

namespace vanila
{
#ifdef DEBUG_LOG_GC

#define ALLOCATOR_LOG(msg, type, ptr)\
std::cout << utils::format(msg " %s in %p\n", Object::label(type), ptr)

#else

#define ALLOCATOR_LOG(msg, type, ptr)

#endif

static VirtualMachine* vm = utils::Singleton<VirtualMachine>::instance();
static GarbageCollector* gc = utils::Singleton<GarbageCollector>::instance();

//! \brief Construct a new Allocator:: Allocator object
Allocator::Allocator() noexcept:
    _bytesAlloceted{0}, _nextGC{1024 * 1024} 
{}

//! \brief allocate a ObjectBoundMethod instance
ObjectBoundMethod* Allocator::allocateBoundMethod(const Value& receiver, Object* method)
{
    void* ptr = this->allocate(sizeof(ObjectBoundMethod));
    ALLOCATOR_LOG("Allocate", ObjectType::BOUND_METHOD, ptr);
    return new(ptr) ObjectBoundMethod(receiver, method);
}

//! \brief allocate a ObjectClass instance
ObjectClass* Allocator::allocateClass(ObjectString* name, bool isNative)
{
    void* ptr = this->allocate(sizeof(ObjectClass));
    ALLOCATOR_LOG("Allocate", ObjectType::CLASS, ptr);
    return new(ptr) ObjectClass(name, isNative);
}

//! \brief find the c-string in vm's stirng hashtable, if not exits return a nullotr
//! \param[in] chars  the start of the c-stirng
//! \param[in] length the length of the c-stirng
ObjectString* Allocator::allocateString(const char* chars, uint32_t length)
{
    return this->allocateString(std::string(chars, length));
}

//! \brief allocate a ObjectString instance
//! \param[in] str std::string object
ObjectString* Allocator::allocateString(std::string str)
{
    uint32_t hash = utils::fnv1aHash(str.c_str(), str.size());
    ObjectString* string = vm->findString(hash);
    if (string != nullptr)
        return string;  

    void* ptr = this->allocate(sizeof(ObjectString));
    ALLOCATOR_LOG("Allocate", ObjectType::STRING, ptr);
    ObjectString* newString = new(ptr) ObjectString(std::move(str));
    
    vm->insertString(hash, newString); 

    return newString;
}

//! \brief allocate a ObjectFunction instance
ObjectFunction* Allocator::allocateFunction()
{
    void* ptr = this->allocate(sizeof(ObjectFunction));
    ALLOCATOR_LOG("Allocate", ObjectType::FUNCTION, ptr);
    return new(ptr) ObjectFunction();
}

//! \brief allocate a ObjectInstance instance
ObjectInstance* Allocator::allocateInstance(ObjectClass* klass)
{
    void* ptr = this->allocate(sizeof(ObjectInstance));
    ALLOCATOR_LOG("Allocate", ObjectType::INSTANCE, ptr);
    return new(ptr) ObjectInstance(klass);
}

//! \brief allocate a ObjectList instance
ObjectList* Allocator::allocateList()
{
    void* ptr = this->allocate(sizeof(ObjectList));
    ALLOCATOR_LOG("Allocate", ObjectType::LIST, ptr);
    return new(ptr) ObjectList();
}

//! \brief allocate a ObjectList instance
ObjectList* Allocator::allocateList(Value* slot, uint16_t elementSize)
{
    void* ptr = this->allocate(sizeof(ObjectList));
    ALLOCATOR_LOG("Allocate", ObjectType::LIST, ptr);
    return new(ptr) ObjectList(std::vector<Value>(slot, slot + elementSize));
}

//! \brief allocate a ObjectList instance
ObjectList* Allocator::allocateList(std::vector<Value> list)
{
    void* ptr = this->allocate(sizeof(ObjectList));
    ALLOCATOR_LOG("Allocate", ObjectType::LIST, ptr);
    return new(ptr) ObjectList(std::move(list));
}

//! \brief allocate a Closure instance
ObjectClosure* Allocator::allocateClosure(ObjectFunction* function)
{
    void* ptr = this->allocate(sizeof(ObjectClosure));
    ALLOCATOR_LOG("Allocate", ObjectType::CLOSURE, ptr);
    return new(ptr) ObjectClosure(function);
}

//! \brief allocate a ObjectDictionay instance
ObjectDictionary* Allocator::allocateDictionary()
{
    void* ptr = this->allocate(sizeof(ObjectDictionary));
    ALLOCATOR_LOG("Allocate", ObjectType::DICTIONARY, ptr);
    return new(ptr) ObjectDictionary();
}

//! \brief allocate a ObjectDictionay instance
ObjectDictionary* Allocator::allocateDictionary(Value* slot, uint16_t pairSize)
{
    void* ptr = this->allocate(sizeof(ObjectDictionary));

    std::map<Value, Value> dict{};
    for (uint16_t i = 0;  i < pairSize; ++i)
        dict[slot[i << 1]] = slot[(i << 1) + 1];

    ALLOCATOR_LOG("Allocate", ObjectType::DICTIONARY, ptr);
    return new(ptr) ObjectDictionary(std::move(dict));
}

//! \brief allocate a Upvalue instance
ObjectUpvalue* Allocator::allocateUpvalue(Value* slot)
{
    void* ptr = this->allocate(sizeof(ObjectUpvalue));
    ALLOCATOR_LOG("Allocate", ObjectType::UPVALUE, ptr);
    return new(ptr) ObjectUpvalue(slot);
}

//! \brief allocate a Native instance
ObjectNative* Allocator::allocateNative(NativeFunction function)
{
    void* ptr = this->allocate(sizeof(ObjectNative));
    ALLOCATOR_LOG("Allocate", ObjectType::NATIVE, ptr);
    return new(ptr) ObjectNative(function);
}

//! \brief allocate a ObjectSet instance
ObjectSet* Allocator::allocateSet()
{
    void* ptr = this->allocate(sizeof(ObjectSet));
    ALLOCATOR_LOG("Allocate", ObjectType::SET, ptr);
    return new(ptr) ObjectSet(); 
}

//! \brief allocate a ObjectSet instance
ObjectSet* Allocator::allocateSet(Value* slot, uint16_t elementSize)
{
    void* ptr = this->allocate(sizeof(ObjectSet));

    std::set<Value> set{};
    for (uint16_t i = 0;  i < elementSize; ++i)
        set.insert(slot[i]);

    ALLOCATOR_LOG("Allocate", ObjectType::SET, ptr);
    return new(ptr) ObjectSet(std::move(set)); 
}

//! \brief deallocate the object instance
void Allocator::deallocateObject(Object* object) noexcept
{
    ALLOCATOR_LOG("Deallocate", object->type(), object);
    
    size_t size = 0;
    switch (object->type())
    {
    case ObjectType::BOUND_METHOD:
        size = sizeof(ObjectBoundMethod);
        break;
    case ObjectType::CLASS:
        size = sizeof(ObjectClass);
        break;
    case ObjectType::CLOSURE:
        size = sizeof(ObjectClosure);
        break;
    case ObjectType::DICTIONARY:
        size = sizeof(ObjectDictionary);
        break;
    case ObjectType::FUNCTION:
        size = sizeof(ObjectFunction);
        break;
    case ObjectType::INSTANCE:
        size = sizeof(ObjectInstance);
        break;
    case ObjectType::UPVALUE:
        size = sizeof(ObjectUpvalue);
        break;
    case ObjectType::NATIVE:
        size = sizeof(ObjectNative);
        break;
    case ObjectType::STRING:
        size = sizeof(ObjectString);
        break;
    case ObjectType::SET:
        size = sizeof(ObjectSet);
        break;
    }

    object->~Object();
    this->deallocate(object, size);
}

//! \brief malloc memory
//! \param[in] size byte size
void* Allocator::allocate(size_t size)
{
#ifdef DEBUG_STRESS_GC
    gc->collectGarbage();
#else
    this->_bytesAlloceted += size;
    if (this->_bytesAlloceted > this->_nextGC)
    {
        gc->collectGarbage();
        this->_nextGC = this->_bytesAlloceted * GC_HEAP_GROW_FACTOR;
    }
#endif

    void* ptr = malloc(size);
    if (ptr == nullptr)
    {
        // do a grabage collect and then try anagin
        gc->collectGarbage();
        void* ptr = malloc(size);
        
        // if still no memory, runtime error
        if (ptr == nullptr)
            vm->runtimeError(utils::format("After allocating %llu byte, no more memory!", this->_bytesAlloceted));
        return ptr;
    } 
    return ptr;
}

//! \brief free memeory
//! \param[in] ptr memory address
//! \param[in] size byte size
void Allocator::deallocate(void* ptr, size_t size) noexcept
{
    if (ptr == nullptr)
        return;
    this->_bytesAlloceted -= size;
    free(ptr);
}

}